/*
 * Copyright 2022-2023 the original author(https://github.com/wj596)
 * 
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */
package org.jsets.fastboot.flowable.component;

import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.flowable.bpmn.BpmnAutoLayout;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.EndEvent;
import org.flowable.bpmn.model.ExclusiveGateway;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.bpmn.model.UserTask;
import org.flowable.editor.language.json.converter.BpmnJsonConverter;
import org.flowable.validation.ProcessValidator;
import org.flowable.validation.ProcessValidatorFactory;
import org.flowable.validation.ValidationError;
import org.jsets.fastboot.flowable.config.FlowableConfig;
import org.jsets.fastboot.flowable.model.dto.DingModelNode;
import org.jsets.fastboot.flowable.model.dto.DingModelNodeData;
import org.jsets.fastboot.flowable.model.dto.DingModelNodeMember;
import org.jsets.fastboot.flowable.model.entity.CompositeModel;
import org.jsets.fastboot.flowable.model.enums.MemberType;
import org.jsets.fastboot.util.CollectionUtils;
import org.jsets.fastboot.util.JsonUtils;
import org.jsets.fastboot.util.StringUtils;
import org.springframework.stereotype.Component;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;

/**
 * 钉钉模型转换器
 * @author wangjie (https://github.com/wj596)
 * @date 2021.06.25 11:23
 */
@Slf4j
@Component
public class DingModelConvertor {

	protected static final String SEQUENCE_FLOW = "0";
	protected static final String START_EVENT = "1";
	protected static final String USER_TASK = "2";
	protected static final String EXCLUSIVE_GATEWAY = "3";
	protected static final String END_EVENT = "9";
	
	protected static final String START_NODE_ID = "startevent1";
	protected static final String END_NODE_ID = "endevent1";
	protected static final String END_NODE_TITLE = "结束";

	protected static final AtomicInteger startEventIndex = new AtomicInteger();
	protected static final AtomicInteger userTaskIndex = new AtomicInteger();
	protected static final AtomicInteger exclusiveGatewayIndex = new AtomicInteger();
	protected static final AtomicInteger flowIndex = new AtomicInteger();
	protected static final AtomicInteger endEventIndex = new AtomicInteger();
	
	private final BpmnJsonConverter bpmnJsonConverter = new BpmnJsonConverter();
	private final BpmnXMLConverter bpmnXMLConverter = new BpmnXMLConverter();

	public BpmnJsonConverter getBpmnJsonConverter() {
		return bpmnJsonConverter;
	}

	public BpmnXMLConverter getBpmnXMLConverter() {
		return bpmnXMLConverter;
	}

	public BpmnModel convert(CompositeModel compositeModel, boolean addEditNode) {
		List<DingModelNode> nodes = Lists.newArrayList();
		if (addEditNode) {
			DingModelNode editNode = new DingModelNode();
			editNode.setType(USER_TASK);
			editNode.setId("edit_" + StringUtils.getUUID());
			editNode.setTitle("编辑表单");
			DingModelNodeData editNodeData = new DingModelNodeData();
			editNodeData.setMemberType(MemberType.expression);
			editNodeData.setMemberExpression("${" + FlowableConfig.PROCESS_START_USER_KEY + "}");
			editNode.setData(editNodeData);
			editNode.getPids().add(START_NODE_ID);
			for (DingModelNode temp : compositeModel.getNodes()) {
				if (temp.getPids().contains(START_NODE_ID)) {
					DingModelNode specific = JsonUtils.deepCopy(temp);
					Set<String> pids = specific.getPids();
					pids.remove(START_NODE_ID);
					pids.add(editNode.getId());
					specific.setPids(pids);
					nodes.add(specific);
				} else {
					nodes.add(temp);
				}
			}
			nodes.add(1, editNode);
		} else {
			nodes.addAll(compositeModel.getNodes());
		}

		DingModelNode endNode = new DingModelNode();
		endNode.setId(END_NODE_ID);
		endNode.setTitle(END_NODE_TITLE);
		for(DingModelNode parent:nodes) {
			for(DingModelNode item:nodes) {
				if(CollectionUtils.notEmpty(item.getPids())) {
					if(item.getPids().contains(parent.getId())) {
						parent.getTargets().add(item.getId());
					}
				}
			}
			if(CollectionUtils.isEmpty(parent.getTargets())) {
				parent.getTargets().add(endNode.getId());
				endNode.getPids().add(parent.getId());
			}
		}
		nodes.add(endNode);
		//System.out.println("第一遍整理： ");
		//System.out.println(JsonUtils.toIndentJson(nodes));
		
		Set<String> gateways = Sets.newHashSet();
		for(DingModelNode node:nodes) {
			Set<String> pids = Sets.newHashSet();
			Set<String> targets = Sets.newHashSet();
			node.getPids().forEach(pid->{
				if(null!=pid) {
					DingModelNode p = this.filterNode(nodes, pid);
					if(EXCLUSIVE_GATEWAY.equals(p.getType())) {
						pids.add(p.getGroupId());
						gateways.add(p.getGroupId());
					}else {
						pids.add(p.getId());
					}
				}
			});
			node.getTargets().forEach(tid->{
				if(null!=tid) {
					DingModelNode t = this.filterNode(nodes, tid);
					if(EXCLUSIVE_GATEWAY.equals(t.getType())) {
						targets.add(t.getGroupId());
						gateways.add(t.getGroupId());
					}else {
						targets.add(t.getId());
					}
				}
			});
			node.setPids(pids);
			node.setTargets(targets);
		}
		//System.out.println("第二遍整理： ");
		//System.out.println(JsonUtils.toIndentJson(nodes));
		
		BpmnModel bpmnModel = new BpmnModel();
		Process process = new Process();
		bpmnModel.addProcess(process);
		process.setId(compositeModel.getModelKey());
		process.setName(compositeModel.getName());

		for(DingModelNode node:nodes) {
			if(START_EVENT.equals(node.getType())) {
				createStartEvent(process, node);
			}
			if(USER_TASK.equals(node.getType())) {
				createUserTask(process, node);
			}
			if(EXCLUSIVE_GATEWAY.equals(node.getType())) {
				createExclusiveGateway(process, node);
			}
		}
		createEndEvent(process, endNode);
		
		ProcessValidatorFactory processValidatorFactory = new ProcessValidatorFactory();
		ProcessValidator defaultProcessValidator = processValidatorFactory.createDefaultProcessValidator();
		List<ValidationError> validationErrors = defaultProcessValidator.validate(bpmnModel);
		if(CollectionUtils.notEmpty(validationErrors)) {
			log.error("流程模型验证失败： ");
			validationErrors.forEach(ve->{
				log.error(ve.toString());
			});
			throw new RuntimeException("流程验证失败");
		}else {
			log.error("流程模型验证无错误");
		}

		//自动布局
        BpmnAutoLayout bpmnLayout = new BpmnAutoLayout(bpmnModel);
        bpmnLayout.execute();
		
		byte[] convertToXML = this.getBpmnXMLConverter().convertToXML(bpmnModel);
		String bytes = new String(convertToXML);
		log.info("钉钉流程模型[{}]转换为Bpmn模型成功", compositeModel.getName());
		log.info(bytes);
		return bpmnModel;
	}
	
	public void createStartEvent(Process process, DingModelNode node) {
		List<SequenceFlow> outgoingFlows = Lists.newArrayList();
		StartEvent startEvent = new StartEvent();
		startEvent.setId(node.getId());
		startEvent.setName(node.getTitle());
		node.getTargets().forEach(target->{
			SequenceFlow sequence = this.createSequenceFlow(node.getId(), target);
			addSequenceFlow(process, sequence);
			outgoingFlows.add(sequence);
		});
		startEvent.setOutgoingFlows(outgoingFlows);
		process.addFlowElement(startEvent);
	}
	
	public void createEndEvent(Process process, DingModelNode node) {
		List<SequenceFlow> incomingFlows = Lists.newArrayList();
		EndEvent endEvent = new EndEvent();
		endEvent.setId(node.getId());
		endEvent.setName(node.getTitle());
		node.getPids().forEach(source->{
			SequenceFlow sequence = this.createSequenceFlow(source, node.getId());
			addSequenceFlow(process, sequence);
			incomingFlows.add(sequence);
		});
		endEvent.setIncomingFlows(incomingFlows);
		process.addFlowElement(endEvent);
	}
	
	public void createUserTask(Process process, DingModelNode node) {
		List<SequenceFlow> incomingFlows = Lists.newArrayList();
		List<SequenceFlow> outgoingFlows = Lists.newArrayList();
		UserTask userTask = new UserTask();
		userTask.setId(node.getId());
		userTask.setName(node.getTitle());
		//MultiInstanceLoopCharacteristics loopCharacteristics = new MultiInstanceLoopCharacteristics();

		DingModelNodeData data = node.getData();
		if(Objects.nonNull(data)) {
			switch (data.getMemberType()) {
			case user:
				List<DingModelNodeMember> memberList = data.getMemberList();
				Objects.requireNonNull(memberList, "成员列表不能为空");
				if (memberList.size() == 1) {
					userTask.setAssignee(memberList.get(0).getAccount());
				} else {
					if (data.isCountersign()) {// 会签

					} else {// 或签
						List<String> candidateUsers = Lists.newArrayList();
						memberList.forEach(m -> candidateUsers.add(m.getAccount()));
						userTask.setCandidateUsers(candidateUsers);
					}
				}
				break;
			case expression:
				userTask.setAssignee(data.getMemberExpression());
				break;
			default:
				break;
			}
		}

		node.getPids().forEach(source->{
			SequenceFlow sequence = this.createSequenceFlow(source, node.getId());
			addSequenceFlow(process, sequence);
			incomingFlows.add(sequence);
		});
		node.getTargets().forEach(target->{
			SequenceFlow sequence = this.createSequenceFlow(node.getId(), target);	
			addSequenceFlow(process, sequence);
			outgoingFlows.add(sequence);
		});
		userTask.setIncomingFlows(incomingFlows);
		userTask.setOutgoingFlows(outgoingFlows);
		
		process.addFlowElement(userTask);
	}
	
	public void createExclusiveGateway(Process process, DingModelNode node) {
		List<SequenceFlow> incomingFlows = Lists.newArrayList();
		List<SequenceFlow> outgoingFlows = Lists.newArrayList();
		boolean exist = false;
		String groupId = node.getGroupId();
		ExclusiveGateway gateway = null;
		for(FlowElement element : process.getFlowElements()) {
			if(element instanceof ExclusiveGateway) {
				ExclusiveGateway item = ((ExclusiveGateway) element);
				if(groupId.equals(item.getId())) {
					incomingFlows.addAll(item.getIncomingFlows());
					outgoingFlows.addAll(item.getOutgoingFlows());
					gateway = item;
					exist = true;
					break;
				}
			}
		}
		if(!exist) {
			gateway = new ExclusiveGateway();
			gateway.setId(groupId);
		}
		
		node.getPids().forEach(source->{
			SequenceFlow sequence = this.createSequenceFlow(source, groupId);	
			addSequenceFlow(process, sequence);
			if(!includeSequenceFlow(incomingFlows, sequence)) {
				incomingFlows.add(sequence);
			}
		});
		node.getTargets().forEach(target->{
			SequenceFlow sequence = this.createSequenceFlow(groupId, target);
			sequence.setName(node.getTitle());
			if (null != node.getData() && StringUtils.notEmpty(node.getData().getConditionExpression())) {
				sequence.setConditionExpression(node.getData().getConditionExpression());
			}
			addSequenceFlow(process, sequence);
			if (!includeSequenceFlow(outgoingFlows, sequence)) {
				outgoingFlows.add(sequence);
			}
		});
		gateway.setIncomingFlows(incomingFlows);
		gateway.setOutgoingFlows(outgoingFlows);
		if(!exist) {
			process.addFlowElement(gateway);
		}
	}
	
	
	public DingModelNode filterNode(List<DingModelNode> nodes, String id) {
		for(DingModelNode node : nodes) {
			if(id.equals(node.getId())) {
				return node;
			}
		}
		return null;
	}
	
	public void addSequenceFlow(Process process, SequenceFlow sequence) {
		boolean notExist = true;
		for(FlowElement ele : process.getFlowElements()) {
			if(ele instanceof SequenceFlow) {
				SequenceFlow tt = ((SequenceFlow) ele);
				if(this.sequenceFlowEquals(tt, sequence)) {
					notExist = false;
					break;
				}
			}
		}
		if(notExist) {
			process.addFlowElement(sequence);
		}
	}
	
	protected SequenceFlow createSequenceFlow(String sourceRef, String targetRef) {
		SequenceFlow sequence = new SequenceFlow();
		String sequenceId = this.getElementId(SEQUENCE_FLOW);
		sequence.setId(sequenceId);
		sequence.setSourceRef(sourceRef);
		sequence.setTargetRef(targetRef);
		return sequence;
	}
	
	protected boolean includeSequenceFlow(List<SequenceFlow> sequences, SequenceFlow sequence) {
		for(SequenceFlow ele : sequences) {
			if(this.sequenceFlowEquals(ele, sequence)) {
				return true;
			}
		}
		return false;
	}
	
	protected boolean sequenceFlowEquals(SequenceFlow source, SequenceFlow target) {
		if(source.getSourceRef().equals(target.getSourceRef()) && source.getTargetRef().equals(target.getTargetRef())) {
			return true;
		}
		return false;
	}

	protected String getElementId(String nodeType) {
		String elementId = "";
		switch (nodeType) {
		case SEQUENCE_FLOW:
			elementId = "flow" + flowIndex.incrementAndGet();
			break;
		case START_EVENT:
			elementId = "startevent" + startEventIndex.incrementAndGet();
			break;
		case USER_TASK:
			elementId = "usertask" + userTaskIndex.incrementAndGet();
			break;
		case EXCLUSIVE_GATEWAY:
			elementId = "exclusivegateway" + exclusiveGatewayIndex.incrementAndGet();
			break;
		case END_EVENT:
			elementId = "endevent" + endEventIndex.incrementAndGet();
			break;
		default:
			break;
		}
		return elementId;
	}
	
}
